﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using Urb.Weather.Core.MeasureUnit;
using System.Globalization;
using Urb.Weather.Core;
using Urb.Weather.Core.Util;

namespace Urb.Weather.Yahoo
{
    public class YahooResponseAnalyzer
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public YahooWeatherData Analyze(string response)
        {
            XPathNavigator navigator = XmlResponseAnalyzerHelper.ValidateResponseParametersAndCreateNavigator(response);
            
            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(navigator.NameTable);
            namespaceManager.AddNamespace("yweather", "http://xml.weather.yahoo.com/ns/rss/1.0");
            namespaceManager.AddNamespace("geo", "http://www.w3.org/2003/01/geo/wgs84_pos#");
            
            YahooWeatherData result = new YahooWeatherData();

            result.SourceXml = response;

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/lastBuildDate", (v) => result.LastBuildDate = v.ConvertToDateTimeOffset());
            
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:location/@city", (v) => result.LocationCity = v);
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:location/@city", (v) => result.LocationCity = v);
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:location/@region", (v) => result.LocationRegion = v);
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:location/@country", (v) => result.LocationCountry = v);            

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:units/@temperature", (v) => result.TemperatureUnit = v.ConvertToTemperatureUnit());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:units/@distance", (v) => result.DistanceUnit = v.ConvertToDistanceUnit());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:units/@pressure", (v) => result.PressureUnit = v.ConvertToPressureUnit());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:units/@speed", (v) => result.SpeedUnit = v.ConvertToSpeedUnit());

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:wind/@chill", (v) => result.WindChill = v.ConvertToShort());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:wind/@direction", (v) => result.WindDirection = v.ConvertToShort());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:wind/@speed", (v) => result.WindSpeed = v.ConvertToDecimal());

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:atmosphere/@humidity", (v) => result.Humidity = v.ConvertToByte());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:atmosphere/@visibility", (v) => result.Visibility = v.ConvertToDecimal());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:atmosphere/@pressure", (v) => result.Pressure = v.ConvertToDecimal());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:atmosphere/@rising", (v) => result.PressureRising = v.ConvertToByte());

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:astronomy/@sunrise", (v) => result.Sunrise = v);
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/yweather:astronomy/@sunset", (v) => result.Sunset = v);

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/geo:lat", (v) => result.Latitude = v.ConvertToDecimal());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/geo:long", (v) => result.Longitude = v.ConvertToDecimal());

            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/yweather:condition/@text", (v) => result.ConditionsDescription = v);
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/yweather:condition/@code", (v) => result.ConditionsCode = v.ConvertToYahooWeatherConditionCode());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/yweather:condition/@temp", (v) => result.Temperature = v.ConvertToShort());
            navigator.ReadSingleNode(
                namespaceManager, "/rss/channel/item/yweather:condition/@date", (v) => result.Date = v.ConvertToDateTimeOffset());

            AnalyzeForecast(navigator, namespaceManager, result);

            return result;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public WeatherData Convert(YahooWeatherData yahooWeatherData)
        {
            if (yahooWeatherData == null)
                throw new ArgumentNullException("yahooWeatherData");

            var data = new WeatherData();

            data.MeasurementTime = yahooWeatherData.Date;

            data.TemperatureUnit = yahooWeatherData.TemperatureUnit;
            data.Temperature = yahooWeatherData.Temperature;

            data.PressureUnit = yahooWeatherData.PressureUnit;
            data.Pressure = yahooWeatherData.Pressure;

            data.AtmosphereConditions = Convert(yahooWeatherData.ConditionsCode);

            data.Forecasts = Convert(yahooWeatherData.Date, yahooWeatherData.TemperatureUnit, yahooWeatherData.Forecasts);

            return data;
        }

        private static void AnalyzeForecast(XPathNavigator navigator, XmlNamespaceManager namespaceManager, YahooWeatherData result)
        {
            XPathExpression expression = navigator.Compile("/rss/channel/item/yweather:forecast");

            expression.SetContext(namespaceManager);

            XPathNodeIterator iterator = navigator.Select(expression);

            List<YahooWeatherForecastData> forecastDatas = new List<YahooWeatherForecastData>();

            if (iterator != null)
            {
                foreach(XPathNavigator nodeNavigator in iterator)
                {
                    YahooWeatherForecastData forecastData = new YahooWeatherForecastData();

                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@day", (v) => forecastData.Day = v.ConvertToDayOfWeek());
                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@date", (v) => forecastData.Date = v.ConvertToDate());
                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@low", (v) => forecastData.TemperatureLow = v.ConvertToShort());
                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@high", (v) => forecastData.TemperatureHigh = v.ConvertToShort());
                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@text", (v) => forecastData.ConditionsDescription = v);
                    nodeNavigator.ReadSingleNode(
                        namespaceManager, "@code", (v) => forecastData.ConditionsCode = v.ConvertToYahooWeatherConditionCode());

                    forecastDatas.Add(forecastData);
                }
            }

            result.Forecasts = forecastDatas.AsReadOnly();
        }

        private static ICollection<WeatherForecastData> Convert(DateTimeOffset? referenceTime, TemperatureUnit? temperatureUnit, ICollection<YahooWeatherForecastData> forecasts)
        {
            List<WeatherForecastData> datas = new List<WeatherForecastData>();

            DateTime? referenceDate = null;
            if (referenceTime.HasValue)
                referenceDate = referenceTime.Value.LocalDateTime.Date;

            if (forecasts != null)
            {
                foreach (YahooWeatherForecastData forecast in forecasts)
                {
                    WeatherForecastData data = new WeatherForecastData();

                    if (referenceDate.HasValue && forecast.Date.HasValue)
                        data.DayOffset = (byte) Math.Round((forecast.Date.Value - referenceDate.Value).TotalDays);

                    data.TemperatureMin = forecast.TemperatureLow;
                    data.TemperatureMax = forecast.TemperatureHigh;
                    data.TemperatureUnit = temperatureUnit;

                    datas.Add(data);
                }
            }

            return datas.AsReadOnly();
        }

        private static AtmosphereConditions Convert(YahooWeatherConditionCode? conditionsCode)
        {
            if (!conditionsCode.HasValue)
                return null;
            
            AtmosphereConditions result = new AtmosphereConditions();

            switch (conditionsCode.Value)
            {
                case YahooWeatherConditionCode.Code_34_fair_day:
                case YahooWeatherConditionCode.Code_32_sunny:
                    result.Code = AtmosphereConditionsCode.FairDay;
                    break;
                case YahooWeatherConditionCode.Code_31_clear_night:
                case YahooWeatherConditionCode.Code_33_fair_night:
                    result.Code = AtmosphereConditionsCode.FairNight;
                    break;
                case YahooWeatherConditionCode.Code_30_partly_cloudy_day:
                case YahooWeatherConditionCode.Code_44_partly_cloudy:
                    result.Code = AtmosphereConditionsCode.FewCloudsDay;
                    break;
                case YahooWeatherConditionCode.Code_29_partly_cloudy_night:
                    result.Code = AtmosphereConditionsCode.FewCloudsNight;
                    break;
                case YahooWeatherConditionCode.Code_11_showers:
                case YahooWeatherConditionCode.Code_12_showers:
                case YahooWeatherConditionCode.Code_01_tropical_storm:
                case YahooWeatherConditionCode.Code_40_scattered_showers:
                    result.Code = AtmosphereConditionsCode.Rain;
                    break;
                case YahooWeatherConditionCode.Code_03_severe_thunderstorms:
                case YahooWeatherConditionCode.Code_04_thunderstorms:
                case YahooWeatherConditionCode.Code_45_thundershowers:
                case YahooWeatherConditionCode.Code_47_isolated_thundershowers:
                case YahooWeatherConditionCode.Code_37_isolated_thunderstorms:
                case YahooWeatherConditionCode.Code_38_scattered_thunderstorms:
                case YahooWeatherConditionCode.Code_39_scattered_thunderstorms:
                    result.Code = AtmosphereConditionsCode.Thunderstorm;
                    break;
                case YahooWeatherConditionCode.Code_13_snow_flurries:
                case YahooWeatherConditionCode.Code_14_light_snow_showers:
                case YahooWeatherConditionCode.Code_15_blowing_snow:
                case YahooWeatherConditionCode.Code_16_snow:
                case YahooWeatherConditionCode.Code_43_heavy_snow:
                case YahooWeatherConditionCode.Code_46_snow_showers:
                case YahooWeatherConditionCode.Code_41_heavy_snow:
                case YahooWeatherConditionCode.Code_42_scattered_snow_showers:
                    result.Code = AtmosphereConditionsCode.Snow;
                    break;
                case YahooWeatherConditionCode.Code_26_cloudy:
                case YahooWeatherConditionCode.Code_27_mostly_cloudy_night:
                case YahooWeatherConditionCode.Code_28_mostly_cloudy_day:
                    result.Code = AtmosphereConditionsCode.Overcast;
                    break;

                case YahooWeatherConditionCode.Code_20_foggy:
                    result.Code = AtmosphereConditionsCode.Fog;
                    break;

                case YahooWeatherConditionCode.Code_00_tornado:
                case YahooWeatherConditionCode.Code_02_hurricane:
                case YahooWeatherConditionCode.Code_05_mixed_rain_and_snow:
                case YahooWeatherConditionCode.Code_06_mixed_rain_and_sleet:
                case YahooWeatherConditionCode.Code_07_mixed_snow_and_sleet:
                case YahooWeatherConditionCode.Code_08_freezing_drizzle:
                case YahooWeatherConditionCode.Code_09_drizzle:
                case YahooWeatherConditionCode.Code_10_freezing_rain:
                case YahooWeatherConditionCode.Code_17_hail:
                case YahooWeatherConditionCode.Code_18_sleet:
                case YahooWeatherConditionCode.Code_19_dust:
                case YahooWeatherConditionCode.Code_21_haze:
                case YahooWeatherConditionCode.Code_22_smoky:
                case YahooWeatherConditionCode.Code_23_blustery:
                case YahooWeatherConditionCode.Code_24_windy:
                case YahooWeatherConditionCode.Code_25_cold:
                case YahooWeatherConditionCode.Code_35_mixed_rain_and_hail:
                case YahooWeatherConditionCode.Code_36_hot:
                case YahooWeatherConditionCode.Code_3200_not_available:
                    break;
            }

            StringBuilder description = null;

            switch (conditionsCode.Value)
            {
                case YahooWeatherConditionCode.Code_3200_not_available:
                    description = new StringBuilder(conditionsCode.Value.ToString().Substring(10));
                    break;
                default:
                    description = new StringBuilder(conditionsCode.Value.ToString().Substring(8));
                    break;
            }

            description.Replace('_', ' ');
            description[0] = char.ToUpper(description[0], CultureInfo.InvariantCulture);

            result.Description = description.ToString();

            return result;
        }
    }
}
